String类为什么是final的?
- 从设计安全上讲,确保不会在子类中改变语义,String类是final类,这意味着不允许任何人定义String的子类,也就是说,如果有一个String的引用,它引用的一定是String对象,而不可能是其他类的对象
- 从效率上讲,设计成final,JVM才不用对相关方法在虚函数表中查询,而直接定位到String类的相关方法上,提高了执行效率
HashMap和Hashtable的区别
两者最主要的区别是Hashtable是线程安全的,而HashMap是非线程安全的
Hashtable的实现方法里都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用
Collections.synchronizedMap()
方法来获取一个线程安全的集合HashMap可以使用null作为key,而Hashtable则不允许使用null作为key
HashMap的初始容量是16,Hashtable的初始容量是11,两者的填充因子默认是0.75,HashMap扩容时是当前容量翻倍即
capacity*2
,而Hashtable扩容时是容量翻倍+1即capacity*2 + 1
HashMap和Hashtable的实现原理
HashMap和Hashtable的底层实现都是数组+链表结构实现的,添加、删除、获取元素时都是先计算hash,根据hash和table.length计算index也就是table数组的下标,然后进行相应操作
class.forName()和classLoader的区别
class.forName()除了将类的.class文件加载到jvm中之外,还会对类就行解释,执行类中的static块,而classLoader只干一件事情,就是将.class文件加载到jvm中,不会指定static中的内容,只有在newInstance才会去执行static块
Session和Cookie的区别和联系
Cookie实际上是一小段的文本信息,客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie,客户端会把Cookie保存起来。
当浏览器再次请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器,服务器检查该Cookie,以此来辨认用户状态,服务器还可以根据需要修改Cookie的内容。
Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器上,而Session保存在服务器上,客户端浏览器访问服务器的时候,服务器把客户信息以某种形式记录在服务器上,这就是Session,客户端浏览器再次访问时只需要从该Session中查找该客户的状态即可。每个用户访问服务器都会建立一个Session,用户和服务器建立连接的同时,服务器会自动会其分配一个SessionId。
什么东西可以让你每次请求都把SessionId自动带到服务器呢?显然是Cookie。当程序需要为某个客户端的请求创建一个Session时,服务器会首先检查这个客户端的请求中是否已经包含了sessionId,如果已包含则说明以前已经为此客户端创建过session,服务器就按照sessionId把这个session检索出来使用(检索不到,会新建一个),如果客户端请求不包含sessionId,则为此客户端创建一个session并且生成一个与此session相关联的sessionId,这个sessionId将被在本次响应中返回给客户端保存。
如果客户端禁用了cookie,通常有两种方法实现session而不依赖cookie:
- URL重写,就是把sessionId直接附加到URL路径的后面
- 表单隐藏字段,就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把sessionId床底回服务器。
Session的生命周期
Session存储在服务端,一般放置在服务器的内存中(为了高速存取),Session在用户第一次访问服务器时创建,需要注意的是只有访问JSP、Servlet等程序时才会创建Session,只访问HTML、IMAGE等静态资源并不会创建Session。
服务器会把长时间没有活动的Session从服务器内存中移除,此时Session失效,Tomcat中Session的默认失效时间是20分钟,当然你也可以调用Session的invalidate方法。
Session对浏览器的要求
虽然Session保存在服务端,对客户端是透明的,它的正常运行仍然需要客户端浏览器的支持,这是因为Session需要时会用Cookie作为识别标志。Http协议是无状态的,Session不能依据HTTP连接来判断是否为同一客户,因此服务器向客户端浏览器发送一个名为JSESSIONID的Cookie,它的值为该Session的Id,Session一看该Cookie来识别是否为同一用户。
该Cookie是服务器自动生成的,它的maxAge属性为-1,表示仅当前浏览器内有效,并且各浏览器窗口间不共享,关闭浏览器会失效,因此同一机器的两个浏览器窗口访问服务器时会生成两个不同的Session,但是由浏览器窗口内的链接、脚本打开的新窗口(也就是说不是双击桌面浏览器图标等打开的窗口)除外,这类子窗口会共享父窗口的Cookie,因此会共享一个Session。
新开的浏览器窗口会生成新的Session,但子窗口除外,子窗口会共用父窗口的Session,例如,在链接上右击,在弹出的快捷菜单上选择在“新窗口中打开”时,子窗口便可以访问父窗口的Session。
Struts2和SpringMVC
- Struts2是类级别的拦截,一个类对应一个request上下文,SpringMVC是方法级别的拦截,一个方法对应一个request上下文
- SpringMVC的方法之间基本上是独立的,独享request response数据,请求数据通过参数获取,处理结果通过ModelMap交回给框架,方法之间不共享变量
- SpringMVC的入口是Servlet,而Struts2是filter
- Struts2需要定义属性来获取请求中参数的数据,而属性在一个类的方法间是共享的(方法间不能独享request、response数据),而SpringMVC中请求参数与Controller中方法的形参自动配对,方法间可以独享request、response数据
- SpringMVC集成了Ajax,使用非常方便,只需一个注解
@ResponseBody
就可以实现,然后直接返回响应文本即可,而Struts2拦截器集成了Ajax,在Action中处理一般需要安装插件或者自己写代码集成进去。
Spring中BeanFactory和ApplicationContext的区别
- BeanFactory采用的是延迟加载的方式来注入Bean的,而ApplicationContext是在容器启动时一次性创建所有的Bean
- BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者的区别是BeanFactory需要手动注册,而ApplicationContext则是自动注册
- BeanFactory是Spring中比较原始的Factory,如XMLBeanFactory就是一种典型的BeanFactory,原始的BeanFactory无法支持Spring的许多插件,如AOP功能、WEB应用等
- ApplicationContext接口由BeanFactory接口派生而来,因而提供BeanFactory所有的功能
Spring循环注入
构造器循环依赖
表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。
Spring容器将每一个正在创建的Bean标识符放在一个“当前创建bean池”中,bean标识符在创建过程中将一直保持在这个池中,因此如果在创建bean过程中发现自己已经在“当前创建bean池”里时,将抛出BeanCurrentlyInCreationException异常表示循环依赖,而对于创建完毕的bean将从“当前创建bean池”中清除掉。
setter循环依赖
表示通过setter注入方式构成的循环依赖,对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(如setter注入)的bean来完成的,而且只能解决单例作用域的bean循环依赖
prototype范围的依赖处理
对于prototype作用域bean,Spring容器无法完成循环依赖注入,因为Spring容器不进行缓存prototype作用域的bean,因此无法提前暴露一个创建中的bean
Spring事务管理
Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。
Spring事务管理器的接口是
org.springframework.transaction.PlatformTransactionManager
,通过这个接口Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了:12345678Public interface PlatformTransactionManager()...{/* 由TransactionDefinition得到TransactionStatus对象 */TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;/* 提交 */Void commit(TransactionStatus status) throws TransactionException;/* 回滚 */Void rollback(TransactionStatus status) throws TransactionException;}JDBC事务
如果应用程序中直接使用JDBC来进行持久化,DataSourceTransactionManager会为你处理事务边界,为了使用DataSourceTransactionManager,你需要使用如下的XML将其装配到应用程序的上下文定义中:
123<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource" /></bean>Hibernate事务
如果应用程序的持久化是通过Hibernate实现的,那么你需要使用HibernateTransactionManager,对于Hibernate3,需要在Spring上下文定义中添加如下的声明:
123<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"><property name="sessionFactory" ref="sessionFactory" /></bean>Java持久化API事务(JPA)
如果你需要使用JpaTransactionManager来处理事务,你需要在Spring中这样配置:
123<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"><property name="sessionFactory" ref="sessionFactory" /></bean>Java原生API事务
如果你没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),你就需要使用JtaTransactionManager:
123<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"><property name="transactionManagerName" value="java:/TransactionManager" /></bean>
Java创建线程之后,直接调用start()方法和run()的区别
start与run方法的主要区别在于当程序调用start方法一个新线程将会被创建,并且在run方法中的代码将会在线程上执行,然而如果你直接调用run方法,程序并不会创建新线程,run方法内部的代码将在当前线程上运行
Java中的线程池
- new Thread的弊端
- 每次new Thread新建对象性能差
- 线程缺少统一管理,可能无限制创建线程,相互之间竞争
- 缺乏更多功能,如定时执行、定期执行、线程中断
- 使用线程池的好处
- 重用存在的线程,减少对象的创建、消亡的开销,性能佳
- 可有效控制最大并发线程数,提高系统资源的使用率
- 提供定时执行、定期执行、单线程、并发数控制等功能
- Java通过Executors提供四种线程池:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,如无可回收,则新建线程
- newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
- newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行
- newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行。
- new Thread的弊端
Spring中controller默认是单例!
Spring的controller为什么默认是单例呢?原因有二:
- 为了性能
- 不需要多例
单例不需要每次都new,自然性能要高,对于第二个原因,如果你给controller中定义了很多的属性,那么单例肯定会出现竞争访问,因此,只要在controller中不定义属性,那么单例就是安全的
synchronized和Lock
synchronized
需要对一个方法进行同步,那么只需在方法的签名添加一个synchronized关键字:
123public synchronized void test() {}synchronized也可以用在一个代码块上:
12345public void test() {synchronized(obj) {...}}synchronized用在方法和代码块上有什么区别呢?
synchronized用在方法签名上,当某个线程调用此方法时,会获取该实例的对象锁,方法未结束之前,其他线程只能去等待,当这个方法执行完后,才会释放对象锁,其他线程才有机会去抢占这把锁,但是发生这一切的基础应当是所有线程使用的同一个对象实例,才能实现互斥的现象。
但是如果该方法为类方法,即其修饰符为static,那么synchronized意味着某个调用此方法的线程当前会拥有该类的锁,只要该线程持续在当前方法内运行,其他线程依然无法获取方法的使用权。
而当synchronized用在代码块上时,就会拥有obj对象的对象锁,如果多个线程共享同一个Object对象,那么此时就会产生互斥,特别的,当obj == this时,表示当前调用该方法的实例对象,即:
123456public void test() {...synchronized(this) {...}}此时,其效果等同于:
123public synchronized void test() {...}使用synchronized代码块,可以只对需要同步的代码进行同步,这样可以大大提高效率。
ReentrantLock
ReentrantLock 与synchronized有相同的并发性和内存语义,还包含了中断锁等候和定时锁等候,意味着线程A如果先获得了对象obj的锁,那么线程B可以在等待指定时间内依然无法获取锁,那么就会自动放弃该锁。
但是由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock();
使用建议
在并发量比较小的情况下使用synchronized是个不错的选择,但是在并发量比较高的情况下其性能下降很严重,此时ReentrantLock是个不错的选择。
wait()与notify()、notifyAll()
这三个方法都是Object的方法,并不是线程的方法。
wait():释放占用的对象锁,线程进入等待池,释放CPU,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是线程调用此方法后会休眠一段时间,休眠期间会暂时释放CPU,但不释放对象锁,也就是说,在休眠期间,其他线程依然无法进入此代码内部,休眠结束,线程重新获得CPU,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会。
notify():该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码执行完毕,才会释放对象锁,JVM会在等待的线程中调用一个线程去获取对象锁,执行代码,需要注意的是,wait()和notify()必须在synchronized代码块中调用。
如何通过反射来创建对象?
通过类对象调用newInstance()方法,适用于无参构造方法,例如:
1String.class.newInstance();1234567891011121314151617public class Solution {public static void main(String[] args) throws Exception {Solution solution = Solution.class.newInstance();Solution solution2 = solution.getClass().newInstance();Class solutionClass = Class.forName("Solution");Solution solution3 = (Solution) solutionClass.newInstance();System.out.println(solution instanceof Solution); //trueSystem.out.println(solution2 instanceof Solution); //trueSystem.out.println(solution3 instanceof Solution); //true}}通过类对象的getConstructor()或getDeclaredConstructor()方法获得构造器对象并调用其newInstance()方法来创建对象,适用于无参和有参的构造方法。
123456789101112131415161718192021222324252627282930313233343536public class Solution {private String str;private int num;public Solution() {}public Solution(String str, int num) {this.str = str;this.num = num;}public Solution(String str) {this.str = str;}public static void main(String[] args) throws Exception {Class[] classes = new Class[] { String.class, int.class };Solution solution = Solution.class.getConstructor(classes).newInstance("hello1", 10);/* hello1 */System.out.println(solution.str);Solution solution2 = solution.getClass().getDeclaredConstructor(String.class).newInstance("hello2");/* hello2 */System.out.println(solution2.str);/* 无参也可用getConstructor() */Solution solution3 = (Solution) Class.forName("Solution").getConstructor().newInstance();/* true */System.out.println(solution3 instanceof Solution);}}getConstructor()和getDeclaredConstructor()区别:
getDeclaredConstructor(Class<?>… parameterTypes)
这个方法会返回制定参数类型的所有构造器,包括public的和非public的,当然也包括private的。
getDeclaredConstructors()的返回结果就没有参数类型的过滤了。再来看getConstructor(Class<?>… parameterTypes)
这个方法返回的是上面那个方法返回结果的子集,只返回制定参数类型访问权限是public的构造器。getConstructors()的返回结果同样也没有参数类型的过滤。
CountDownLatch
现有一个任务A,它要等待其他4个任务执行完毕后才能执行,此时就可以使用CountDownLatch来实现:
1234public CountDonwLatch(int count); // 构造方法public void await() throws InterruptedException; // 调用await()方法的线程将会被挂起,它会等到直到count值为0才继续执行public boolean await(long timeout, TimeUnit unit) throws InterruptedException; // 和await()相似,但是只不过等待一定的时间后count值还没变0的话会继续执行public void countDown(); // 将count值减1例子如下:
12345678910111213141516171819202122232425262728293031323334353637383940public class Test {public static void main(String[] args) {final CountDownLatch latch = new CountDownLatch(2);new Thread(){public void run() {try {System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");Thread.sleep(3000);System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");latch.countDown();} catch (InterruptedException e) {e.printStackTrace();}};}.start();new Thread(){public void run() {try {System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");Thread.sleep(3000);System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");latch.countDown();} catch (InterruptedException e) {e.printStackTrace();}};}.start();try {System.out.println("等待2个子线程执行完毕...");latch.await();System.out.println("2个子线程已经执行完毕");System.out.println("继续执行主线程");} catch (InterruptedException e) {e.printStackTrace();}}}执行结果如下:
1234567线程Thread-0正在执行线程Thread-1正在执行等待2个子线程执行完毕...线程Thread-0执行完毕线程Thread-1执行完毕2个子线程已经执行完毕继续执行主线程String、StringBuffer和StringBuilder的区别
String
String为字符串常量,字符串长度不可变
StringBuffer
字符串变量,其为线程安全的,如果要频繁对字符串内容进行修改,出于效率考虑最好使用StringBuffer,如果要转成String类型,可以调用StringBuffer的toString()方法
StringBuilder
字符串变量,非线程安全
因此,如果要操作少量的数据,用String,单线程操作大量数据,用StringBuilder,多线程操作大量数据,用StringBuffer。
TreeMap的内部实现原理
TreeMap的结构是红黑树,
SpringMVC的controller是单例吗?
尽量不要在Controller里面去定义属性,如果在特殊情况下需要定义属性的话,那么就在类上面加上注解
@Scope("prototype")
改为多例的模式,以前Struts2是基于类的属性进行开发的,定义属性可以整个类通用,所以默认是多例,不然多线程访问肯定是不安全的。但是SpringMVC是基于方法的开发,都是用形参接收值,一个方法结束参数就销毁了,多线程访问都会有一些内存空间产生,里面的参数是不会共有的,所以SpringMVC默认使用了单例所以controller里面不适合在类里面定义属性,只要controller中不定义属性,那么单例完全是安全的。springmvc这样设计主要的原因也是为了提高程序的性能和以后程序的维护只针对业务的维护就行,要是struts的属性定义多了,都不知道哪个方法用了这个属性,对以后程序的维护还是很麻烦的。classpath
JDK安装完后如果没有设置环境变量CLASSPATH,则系统默认的类路径包括java系统类路径和当前目录。比如你的当前工作目录是E:\,在此目录下有class1.class,你可以直接使用”java class1”运行这个类。但是如果切换到别的目录,再使用”java class1”会出现NoClassDefFoundError,此时需要指定运行参数classpath。使用”java -classpath E:\ class1”运行class1.class。
如果class1引用了其他的类,那么被引用的类也需要在E:\目录下,否则应该在classpath参数中指定被引用类所在的目录,比如class1引用了class2.class,class2.class位于F:\目录下,使用”java -classpath E:\;F:\ class1”来运行class1。
如果被引用的是一个jar文件,那么在classpath中需要指明具体的jar文件,而不能只包括jar文件所在的目录。比如,class1引用了jar1.jar,这个jar文件位于C:\目录下,则使用”java -classpath E:\;C:\jar1.jar class1”来运行class1。
在非class1.class所在的目录下运行class时,classpath参数需要指明class1以及class1所引用的类的路径,如果在class1.class所在目录运行该类,同样需要在classpath中包括当前目录,因为使用-classpath时不再会默认当前目录为类路径。比如在E:\下要使用”java -classpath .;F:\ class1”来运行class1。
“java -classpath e:\ class1”不能写成”java class1 -classpath e:\”。
使用javac编译java文件时,可以使用-classpath指定class1.class所引用的类文件所在的目录。在D:\目录下编译class1.java使用”javac E:\class1.java -classpath F:\”如果引用的是jar文件,同样需要指明jar文件。”javac E:\class1.java -classpath C:\jar1.jar”。
javac -classpath F:\ E:\class1.java可以写成javac E:\class1.java -classpath F:\。
类的实例化顺序
- 执行父类的静态变量赋值和静态代码块
- 执行子类的静态变量赋值和静态代码块
- 执行父类的成员变量赋值语句
- 执行父类构造方法
- 执行子类的成员变量赋值语句
- 执行子类构造方法
索引失效的情况
- 如果条件中有or,即使其中有条件带索引也不会使用,要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
- 对于多列索引,不是使用的第一部分,则不会使用索引
- like查询是以%开头
- 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
- 如果mysql估计使用全表扫描比使用索引快,则不使用索引
受检异常和运行时异常
运行时异常是运行时才会发生的异常,而受检异常是编译时异常。
除了runtimeException以外的异常都属于checkedException,Java编译器要求程序必须捕获或声明受检异常。
Session
Session的典型应用是存放用户的Login信息,如用户名、密码、权限角色等信息。
- Session对象在浏览器中的有效范围
- Session对象只在建立Session对象的窗口中有效
- 在建立Session对象的窗口中新开链接的窗口也有效
- Cookie是在服务器给客户端一个命令后在客户端产生并保存的,它可以存放用户信息,存在客户端硬盘上
- Session和Cookie是不同的,但它们却是相关的,当打开浏览器登入后,会向服务器发出一个命令请求SESSIONID以及页面内容,服务器会返回页面内容和一个没有被使用的SESSIONID让此浏览器使用,当时浏览器就会对返回的SESSIONID进行存储,而当此浏览器再访问任何这个站点的JSP的时候都会给服务器这个SESSIONID,来确认客户端的身份。
- Session对象在浏览器中的有效范围
Linux中查看某个端口是否被占用
lsof
使用命令
lsof -i:端口号
可以查看某个端口是否被占用netstat
使用命令
netstat -anp | grep 端口号
Spring的事务隔离级别
ISOLATION_DEFAULT(一般情况下使用这种配置即可)
ISOLATION_READ_UNCOMMITTED
这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据,这种隔离级别会产生脏读、不可重复读以及幻象读
ISOLATION_READ_COMMITTED
保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据,这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻象读
ISOLATION_REPEATABLE_READ
这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻象读。
什么是不可重复读?(修改引起)
例如:
在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。
与此同时,事务B把张三的工资改为8000,并提交了事务。随后,在事务A中,再次读取张三的工资,此时工资变为8000。在一个事务中前后两次读取的结果并不致,导致了不可重复读。
(大部分数据库缺省的事物隔离级别都不会出现这种状况) 。ISOLATION_SERIALIZABLE
这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行,除了防止脏读、不可重复读外,还避免了幻读。
什么是幻读?(添加新纪录引起)
例如:
A目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。此时,事务B插入一条工资也为5000的记录。这是,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。
大部分数据库缺省的事物隔离级别都会出现这种状况,此种事物隔离级别将带来表级锁)
Iterator遍历Collection时,是fail-fast机制的,即当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程改变了,那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
ArrayList的操作不是线程安全的,所以,建议在单线程中使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
ArrayList实际上是通过一个数组去保存数据的,当我们构造ArrayList时,若使用默认构造函数,则ArrayList的默认容量大小是10;当ArrayList容量不足时,ArrayList会重新设置容量:
新的容量=(原始容量×3)/2 + 1
;ArrayList的克隆函数,即是将全部元素克隆到一个数组中;ArrayList实现java.io.Serializable的方式即是当写入到输出流时,先写入容量,再依次写入每一个元素,当读出输入流时,先读取容量,再依次读取每一个元素遍历ArrayList时,使用随机访问(通过索引号访问)效率最高,其次是for循环遍历,使用迭代器的效率最低
组合索引
组合索引即为由多个列构成的索引,创建组合索引
create index idx_detp on detp(col1, col2, col3, ...)
,则我们称idx_detp索引为组合索引。在组合索引中有一个重要的概念就是引导列, 在上面的例子中col1即为引导列,当我们进行查询时where限制条件必须有引导列。
使用组合索引的情况(where条件中有引导列)
- where col1 =
- where col1 = and col2 =
- where col2 = and col1 =
不会使用索引(where条件中没有引导列)
where col2 = ,这种查询因为没有引导列所以不会使用组合索引。
普通索引
其sql格式是:
1CREATE INDEX IndexName ON `TableName`(`字段名(length)`)或者
1ALTER TABLE TableName ADD INDEX IndexName(`字段名(length)`)唯一索引
与普通索引类似,但是不同的是唯一索引要求所有的类的值是唯一的,这一点和主键索引一样,但是它允许有空值,其sql语句格式是:
1CREATE UNIQUE INDEX IndexName ON `TableName`(`字段名(length)`)主键索引
不允许有空值(在B+TREE的InnoDB引擎中,主键索引起到了至关重要的地位)。主键索引的建立规则是int优于varchar,一般在建表的时候创建,最好是与表的其他字段不相关的列或者是业务不相关的列,一般会设为int,而且是AUTO_INCREMENT自增类型的。
Struts2中action默认是非线程安全的,每次请求在heap中new新的action实例,所以在Struts2中action可以用实例成员变量。
ThreadLocal
ThreadLocal为变量在每个线程中创建了一个副本,那么每个线程都可以访问自己内部的副本变量。
ThreadLocal具有以下一些方法:
1234public T get() {}public void set(T value) {}public void remove() {}protected T initialValue() {}get()方法用来获取ThreadLocal在当前线程中保存的变量副本,set()方法用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本。
String是最基本的数据类型吗?
不是,Java中的基本数据类型有:byte、short、int、long、float、double、char、boolean,其他都是引用类型。
int和Integer的区别
Java为每一个基本数据类型都引入了对应的包装类型,int的包装类型就是Integer,从Java5开始引入了自动装箱/拆箱机制,使得两者可以相互转换。
1234567class AutoUnboxingTest {Integer a = new Integer(3);Integer b = 3; // 将3自动装箱成Integer类型int c = 3;System.out.println(a == b); // false 两个引用没有引用同一对象System.out.println(a == c); // true a自动拆箱成int类型再和c比较}再看下面这个例子:
1234567public class Test03 {public void main(String[] args) {Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;System.out.println(f1 == f2);System.out.println(f3 == f4);}}首先需要注意的是f1、f2、f3、f4四个变量都是Integer对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。
12345public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i);}IntegerCache是Integer的内部类,其代码如下所示:
123456789101112131415161718192021222324252627282930313233private static class IntegerCache {static final int low = -128;static final int high;static final Integer cache[];static {// high value may be configured by propertyint h = 127;String integerCacheHighPropValue =sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");if (integerCacheHighPropValue != null) {try {int i = parseInt(integerCacheHighPropValue);i = Math.max(i, 127);// Maximum array size is Integer.MAX_VALUEh = Math.min(i, Integer.MAX_VALUE - (-low) -1);} catch( NumberFormatException nfe) {// If the property cannot be parsed into an int, ignore it.}}high = h;cache = new Integer[(high - low) + 1];int j = low;for(int k = 0; k < cache.length; k++)cache[k] = new Integer(j++);// range [-128, 127] must be interned (JLS7 5.1.7)assert IntegerCache.high >= 127;}private IntegerCache() {}}简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2的结果是true,而f3==f4的结果是false。
方法区和堆是各个线程共享的内存区域,用于存储已经被JVM加载的类信息,常量,静态变量,JIT编译器编译后的代码等数据。程序中的字面量和常量都是放在常量池中的,常量池是方法区的一部分。
Collection是一个接口,它是Set、List等容器的父接口,Collections是一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。
List、Set、Map是否继承自Collection接口?
List、Set是,Map不是,Map是键值对映射容器,与List和Set有明显的区别,而Set存储的是零散的元素且不允许有重复的元素。
怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串?
12String s1 = "你好"!;String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");如何将字符串类型转换为基本数据类型?
调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)即可返回相应基本类型
如何将基本数据类型转换为字符串?
- 将基本数据类型和空字符串(””)连接(+)即可
- 调用String类中的valueOf方法返回相应字符串
接口可以继承接口,而且支持多重继承
抽象方法是否可同时是静态的、是否可同时是本地方法、是否可同时被synchronized修饰?
都不能,抽象方法需要子类去重写,而静态的方法是属于类的,因此不能被重写,所以抽象方法不能是静态的;本地方法是由本地代码(C代码)实现的方法,而抽象方法是没有实现的,所以也不行;synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是不行的!
Java IO流
字符流(读取和存储纯文本文件)
Java API提供了FileWriter和FileReader类来进行字符流的读写:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253package org.example.io;import java.io.File;import java.io.FileNotFoundException;import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;public class TestFileWriter {public static void main(String[] args) throws Exception {writeToFile();readFromFile();}/*** DOC 从文件里读取数据.** @throws FileNotFoundException* @throws IOException*/private static void readFromFile() throws FileNotFoundException, IOException {File file = new File("E:\\helloworld.txt");// 指定要读取的文件FileReader reader = new FileReader(file);// 获取该文件的输入流char[] bb = new char[1024];// 用来保存每次读取到的字符String str = "";// 用来将每次读取到的字符拼接,当然使用StringBuffer类更好int n;// 每次读取到的字符长度while ((n = reader.read(bb)) != -1) {str += new String(bb, 0, n);}reader.close();// 关闭输入流,释放连接System.out.println(str);}/*** DOC 往文件里写入数据.** @throws IOException*/private static void writeToFile() throws IOException {String writerContent = "hello world,你好世界";// 要写入的文本File file = new File("E:\\helloworld.txt");// 要写入的文本文件if (!file.exists()) {// 如果文件不存在,则创建该文件file.createNewFile();}FileWriter writer = new FileWriter(file);// 获取该文件的输出流writer.write(writerContent);// 写内容writer.flush();// 清空缓冲区,立即将输出流里的内容写到文件里writer.close();// 关闭输出流,施放资源}}字节流(读取和存储图片)
Java API提供了FileInputStream和FileOutputStream来完成字节流的读写:
1234567891011121314151617181920212223242526272829303132package org.example.io;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;public class TestIOStream {/**** DOC 将F盘下的test.jpg文件,读取后,再存到E盘下面.** @param args* @throws Exception*/public static void main(String[] args) throws Exception {FileInputStream in = new FileInputStream(new File("F:\\test.jpg"));// 指定要读取的图片File file = new File("E:\\test.jpg");if (!file.exists()) {// 如果文件不存在,则创建该文件file.createNewFile();}FileOutputStream out = new FileOutputStream(new File("E:\\test.jpg"));// 指定要写入的图片int n = 0;// 每次读取的字节长度byte[] bb = new byte[1024];// 存储每次读取的内容while ((n = in.read(bb)) != -1) {out.write(bb, 0, n);// 将读取的内容,写入到输出流当中}out.close();// 关闭输入输出流in.close();}}
字节流和字符流
首先明确字节和字符的大小:
- 1byte = 8bit
- 1char = 2byte = 16bit(Java默认UTF-16编码)
下面让看下具体看下字节流和字符流:
字节流
Java中的字节流处理的最基本单位是单个字节,它通常用来处理二进制数据,Java中最基本的两个字节流是InputStream和OutputStream,它们分别代表了最基本的输入字节流和输出字节流。InputStream和OutputStream均为抽象类。
InputStream类中定义了一个基本的用于从字节流中读取字节的方法read,这个方法的定义如下:
1public abstract int read() throws IOException;这是一个抽象方法,其功能是从字节流中读取一个字节,若到了末尾则返回-1,否则返回读入的字节。
字符流
Java中字符流处理的最基本的单元是Unicode码元(大小2字节),它通常用来处理文本数据。所谓Unicode码元,也就是一个Unicode代码单元,范围是0x0000~0xFFFF,在以上范围内的每个数字都与一个字符相对应,Java的String类型默认就把字符以Unicode规则编码而后保存在内存中,然而与存储在内存中不同,存储在硬盘上的数据通常有着各种各样的编码方式,使用不同的编码方式,相同的字符会有不同的二进制表示,实际上字符流是这样工作的:
- 输出字符流:把要写入文件的字符序列(实际上是Unicode码元序列)转为指定编码方式下的字节序列,然后再写入到文件中
- 输入字符流:把要读取的字节序列按指定编码方式解码为相应字符序列从而可以存在内存中
编码问题
ASCII
原本对于西方世界来说,1个字节足矣,因为1个字节最多256个符号编码,而英文26个字母再加几个常用符号、标点,256个符号编码足够了,这就是ASCII编码
ISO 8859-1
ISO 8859-1是国际标准化组织定义的法语、芬兰语所用的西欧字符集,也是每个字母或者符号用1个字节表示
中文编码
如果说一个汉字表示一个字符,那么收到的每一个字节不能简单的解码成一个字母了,而是需要好几个字节组成一个汉字,我国的汉字编码现行标准是GB18030,每个字可以由1个、2个或4个字节组成,编码空间161万个字符
Unicode
在出现Unicode之前,几乎每一种文字都有一套自己的编码方式,同一段字节流,在美国是”hello world”,到了国内可能就变成”烫烫烫”了,Unicode的理论就是全世界每个不同语言的不同字符都统一编码,全球通行。最初每个字符占用2个字节,总共65536个字符空间,从第四版开始加入的“扩展字符集”开始使用4个字节编码。
UTF-16
Unicode只是一套符号的编码,但计算机具体怎么读取这套编码呢?比如既然Unicode常规字符集占用2个字节,系统可以每次老老实实的读取两个字节,然后用一个特殊符号告诉系统某个字符属于附加字符集,需要再往后读2个字节,比如说Java系统默认的UTF-16就是这样编码解码的
UTF-8
UTF-16存在的问题就是所有英语字符也被迫使用2个字节来编码,那么就得使用可变长编码UTF-8,其用几位冗余信息告诉系统,当前字符有没有结束,是不是还需要继续往下读下一个字节。
如果一个字节是以“0”开头的,说明是一个ASCII字符,只占用一个字节,如果是“11”开头的,说明这个字符占用多个字节,后续每个“10”打头的字节都是这个字符的一部分。
本小节摘录自Java 中字节流与字符流的区别?。
Java中char默认采用Unicode编码,所以Java中char占2个字节
Java文件的编码可能有多种多样,但Java编译器会自动将这些编码按照Java文件的编码格式正确读取后产生class文件,这里的class文件编码是Unicode编码(具体来说是UTF-16编码)。
因此,在Java代码中定义了一个字符串:
String s = “汉字”
不管在编译前java文件使用何种编码,在编译成class后,它们都是一样的,即Unicode编码表示。
JVM加载class文件读取时候使用Unicode编码方式正确读取class文件,那么原来定义的String s = “汉字”在内存中的表现形式是Unicode编码。
利用序列化实现深复制
把对象写到流里的过程是序列化过程,而把对象从流中读取出来的过程则叫做反序列化过程。
需要注意的是:写在流里的是对象的一个拷贝,而原对象仍然在JVM中。
在Java中深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;public class CloneTest3{public static void main(String[] args) throws Exception{Teacher3 t = new Teacher3();t.setName("Teacher Wang");t.setAge(50);Student3 s1 = new Student3();s1.setAge(20);s1.setName("ZhangSan");s1.setTeacher(t);Student3 s2 = (Student3) s1.deepClone();System.out.println("拷贝得到的信息:");System.out.println(s2.getName());System.out.println(s2.getAge());System.out.println(s2.getTeacher().getName());System.out.println(s2.getTeacher().getAge());System.out.println("---------------------------");// 将复制后的对象的老师信息修改一下:s2.getTeacher().setName("New Teacher Wang");s2.getTeacher().setAge(28);System.out.println("修改了拷贝对象的教师后:");System.out.println("拷贝对象的教师:");System.out.println(s2.getTeacher().getName());System.out.println(s2.getTeacher().getAge());System.out.println("原来对象的教师:");System.out.println(s1.getTeacher().getName());System.out.println(s1.getTeacher().getAge());// 由此证明序列化的方式实现了对象的深拷贝}}class Teacher3 implements Serializable{private String name;private int age;public String getName(){return name;}public void setName(String name){this.name = name;}public int getAge(){return age;}public void setAge(int age){this.age = age;}}class Student3 implements Serializable{private String name;private int age;private Teacher3 teacher;public String getName(){return name;}public void setName(String name){this.name = name;}public int getAge(){return age;}public void setAge(int age){this.age = age;}public Teacher3 getTeacher(){return teacher;}public void setTeacher(Teacher3 teacher){this.teacher = teacher;}public Object deepClone() throws Exception{// 序列化ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(this);// 反序列化ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bis);return ois.readObject();}}Java中非静态内部类对象的创建要依赖外部类对象,对于静态方法,静态方法中没有this,也就是说没有外部类对象,因此无法创建内部类对象。
Spring声明式事务管理默认对运行时异常进行事务管理,而对检查型异常不进行回滚操作,如果异常被try{}catch{}了,事务就不回滚了,如果想让事务回滚必须再往外抛try{}catch{throw Exception}
如果在函数体内用throw抛出了某种异常,最好要在函数名中加throws抛异常声明,然后交给调用它的上层函数进行处理
如果SELECT后面若要UPDATE同一个表单,最好使用SELECT…UPDATE,举个例子:
假设商品表单products 内有一个存放商品数量的quantity ,在订单成立之前必须先确定quantity 商品数量是否足够(quantity>0) ,然后才把数量更新为1。代码如下:
1SELECT quantity FROM products WHERE id=3; UPDATE products SET quantity = 1 WHERE id=3;为什么不安全呢?
少量的状况下或许不会有问题,但是大量的数据存取「铁定」会出问题。如果我们需要在quantity>0 的情况下才能扣库存,假设程序在第一行SELECT 读到的quantity 是2 ,看起来数字没有错,但
是当MySQL 正准备要UPDATE 的时候,可能已经有人把库存扣成0 了,但是程序却浑然不知,将错就错的UPDATE 下去了。因此必须透过的事务机制来确保读取及提交的数据都是正确的。FOR UPDATE仅适用于InnoDB,且必须在事务区块(BEGIN/COMMIT)中才能生效:
123BEGINSELECT * FROM `table_name` WHERE xxx FOR UPDATECOMMIT使用SELECT… FOR UPDATE会把数据给锁住,但是需要注意的是,MySQL InnoDB默认为Row-Level Lock,所以只有明确的指定主键,MySQL才会执行Row Lock(只锁住被选取的数据),否则MySQL将会执行Table Lock(将整个数据表单给锁住)。
本小节内容摘录自mysql select for update。
向枚举中添加新方法
如果打算自定义自己的方法,那么必须在enum实例序列的最后添加一个分号,而且Java要求必须先定义enum实例。
123456789101112131415161718192021222324252627282930313233public enum Color {RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4); // enum实例序列private String name;private int index;private Color(String name, int index) {this.name = name;this.index = index;}public static String getName(int index) {for (Color c : Color.values()) {if (c.getIndex() == index) {return c.name;}}return null;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getIndex() {return index;}public void setIndex(int index) {this.index = index;}}QPS:每秒查询率(Query Per Second)
MySQL中delimiter的作用:
其实就是告诉MySQL解释器,该段命令是否已经结束了,MySQL数据库是否可以执行了,默认情况下delimiter是分号,在命令行客户端中,如果有一行命令以分号结束,那么回车后,MySQL将会执行该命令。
在为可能输入较多的语句,且语句中包含有分号时,默认情况下,不可能等到用户把语句全部输入完成之后再执行整段语句,因为MySQL一遇到分号,它就要自动执行,这种情况下,就需要事先把delimiter换成其他符号,如//或$$。
MySQL命令行中可以使用
system clear;
命令进行清屏计算网络延迟:
光在真空中的传播速度是30万公里每秒,光在玻璃中的传播速度是真空的2/3,假如往返举例为X,那么网络延迟即为:
1X/(300000 * 2 / 3)使用存储过程可以使得整个事务在MySQL端完成,从而减少SQL语句在Java端执行所带来的网络延迟和GC干扰
RPC(Remote Procedure Call):远程过程调用,允许程序调用另一个地址空间的过程或函数,而不用程序员显式编码这个远程调用的细节;
Map是Java中的一个接口,Map.Entry是Map的一个内部接口,Map提供了一些常用方法,如keySet()、entrySet()等方法,keySet()方法返回值是Map中key值的集合,entrySet()方法的返回值也是一个Set集合,此集合的类型为Map.Entry。
Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为
Entry<K,V>
,它表示Map中的一个实体(一个key-value对),接口中有getKey()、getValue()方法。12345678910Set entries = map.entrySet( );if(entries != null) {Iterator iterator = entries.iterator( );while(iterator.hasNext( )) {Map.Entry entry =iterator.next( );Object key = entry.getKey( );Object value = entry.getValue();;....}}自动装箱和自动拆箱
自动装箱
把基本类型用它们对应的引用类型包装起来,使它们具有对象的性质,比如:
1Integer a = 3; // 自动装箱其实编译器调用的是
static Integer valueOf(int i)
这个方法,valueOf(int i)
返回一个表示指定int值的Integer对象。自动拆箱
将Integer这样的引用类型的对象重新简化为基本类型的数据。比如:
1int i = new Integer(2);编译器内部会调用
int intValue()
返回该Integer对象的int值。
<a>
标签的href和onclick属性- 链接的onclick事件先执行,其次是href属性下的动作(页面跳转或javascript伪链接)
- 假设链接中同时存在href和onclick,如果想让href属性下的动作不执行,onclick必须得到一个false的返回值
在创建非静态内部类对象时,一定要先创建相应的外部类对象,这是因为非静态内部类对象有着指向其外部类对象的引用,Java编译器在创建内部类对象时,隐式的把其外部类对象的引用也传进去并一直保存着。
内部类也可以是静态的,但是静态内部类没有了指向外部类的引用,静态内部类中不能访问外部类的非静态成员。
在任何非静态内部类中,都不能有静态数据、静态方法。
Spring之FactoryBean
BeanFactory:工厂类,用于管理Bean的工厂
FactoryBean:一个Bean,不同于普通的Bean的是,它实现了
FactoryBean<T>
接口,根据该Bean的id从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象。在使用Arrays.asList()方法需要注意以下两点:
避免使用基本数据类型数组转换为列表
asList()方法接收的参数是一个泛型的变长参数,但是基本数据类型是无法泛型化的,此时会将基本数据类型的数组当做其参数,而在Java中数组是一个对象,它是可以泛型化的。
asList()方法产生的列表不可操作,不要试图去改变asList()返回的列表
ReentrantLock拥有Synchronized相同的并发性和内存语义,此时还多了锁投票、定时锁等候和中断锁等候,线程A和B都要获取对象O的锁定,假设A获取了对象O的锁,B将等待A释放对O的锁定,如果使用Synchronized,如果A不释放,B将一直等待下去,不能被中断;如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时候后,中断等待,而干别的事情。
Synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控Synchronized的锁定,而且在代码出现异常时,JVM会自动释放锁,但是使用Lock则不行,Lock是通过代码实现的,要保证锁定一定会释放,就必须将unLock()放到finally{}中。
在资源竞争不是很激烈的情况下,Synchronized的性能要高于ReentrantLock,但是在资源竞争激烈的情况下,Synchronized的性能会下降几十倍,但是ReentrantLock的性能能维持常态。
分布式Session
Web应用在单机部署的情况下,Session是被单个应用服务器存储并管理的,由于只有一个应用服务器,用户的所有请求都是通过它进行响应处理的,所以能够很容易实现会话跟踪和保持。随着业务量的增加,系统架构需要作出调整以适应发展的需要,所以会将应用系统部署到多台引用服务器上,用户的请求也会通过负载均衡转发到某个具体的应用服务器上执行,可能会出现在A1系统登录后创建并保存Session,再次发起请求,请求被转发到A2系统上显示未登录的情况,此时单机部署模式下的Session机制已不能满足需求。
所以,在分布式架构或微服务架构下,必须保证一个应用服务器上保存Session后,其他应用服务器可以同步或共享这个Session。
分布式Session管理有以下几种实现方式:
Session复制
在支持Session复制的Web服务器上,通过修改Web服务器的配置,可以实现将Session同步到其他Web服务器上,达到每个Web服务器上都保存一致的Session。
优点:代码上不需要修改
缺点:需要依赖支持的Web服务器,一旦更改成不支持的Web服务器就不能使用了,在数据量很大的情况下不仅占用网络资源,而且会导致延迟。
适用场景:只适用于Web服务器比较少且Session数据量少的情况
可用方案:开源方案tomcat-redis-session-manager,暂不支持Tomcat8。
Session粘带
将用户的每次请求都通过某种方法强制分发到某一个Web服务器上,只要这个Web服务器上存储了对应的Session数据,就可以实现会话跟踪。
Session集中管理
在单独的服务器或者服务器集群上使用缓存技术,如Redis存储Session数据,集中管理所有的Session,所有的Web服务器都从这个存储介质中存取对应的Session,实现Session共享。
优点:可靠性高,减少Web服务器的资源开销
缺点:实现上有些复杂,配置较多
适用场景:Web服务器较多,要求高可用性的情况
可用方案:开源方案Spring Session
基于Cookie管理
这种方式每次发起请求的时候都需要将Session数据放到Cookie中传递给服务端。
优点:不需要依赖额外外部存储,不需要额外配置
缺点:不安全,易被盗取或篡改,Cookie数量和长度有限制,需要消耗更多网络带宽
这四种方式,Session集中管理更加可靠,使用也是最多的。
本小节转自细说分布式Session管理。
MySQL索引
索引的本质
在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高效查找算法,这种数据结构就是索引。
看下面这个例子:
左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上并不一定是物理相邻的)。为了加快Col2的查找,可以维护一个右边所示的二叉查找树,每个节点包含索引键值和一个指向对应数据记录物理地址的指针,这样就可以运用二叉查找在O(log2n)的复杂度内获取到相应数据。
实际的数据库系统几乎没有使用二叉查找树或红黑树实现的。
目前大部分数据库系统及文件系统使用B-树或其变种B+树作为索引结构。
MyISAM引擎使用B+树作为索引结构,叶节点的data域存放的是数据记录的地址:
从上图可以看出,MyISAM的索引文件仅仅保存数据记录的地址,在MyISAM中,主索引和辅助索引(Secondary Key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以是重复的。
InnoDB也是使用B+树作为索引结构,但是InnoDB的数据文件本身就是索引文件,从上文可知,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址,而在InnoDB中,表数据文件本身就是按照B+树组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录,这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。
从上图可以看出,叶节点包含了完整的数据记录,这种索引叫做聚集索引,因为InnoDB的数据文件本身就是按主键索引,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
InnoDB引擎与MyISAM引擎另一个不同点在于InnoDB的辅助索引data域存储相应记录主键的值而不是地址,也就是说,InnoDB的所有辅助索引都引用主键作为data域:
上图中叶子结点的data域存放的都是主键值。这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
使用
SHOW INDEX FROM TABLE_NAME
命令可以查看表上建立了哪些索引。MySQL的查询优化器会自动调整where字句的条件顺序以使用适合的索引。
使用
SELECT DISTINCT(col_name) FROM table_name
可以查看col_name列共有几种不同的取值在使用InnoDB存储引擎时,如果没有特别的必要,请永远使用一个与业务无关的自增字段作为主键。
本小结转自MySQL索引背后的数据结构及算法原理。
数据库三大范式:
- 强调的是列的原子性,即列不能再分成其他几列
- 一个表必须有一个主键,并且没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分
- 不能存在非主键列A依赖于非主键列B,非主键列B依赖于主键的情况,即非主键列必须直接依赖于主键,不能存在传递依赖
ArrayList初始化的默认长度是10
JDK和CGLIB
- JDK动态代理只能针对实现了接口的类生成代理(实例化一个类),此时代理对象和目标对象实现了相同的接口,目标对象作为代理对象的一个属性,具体接口实现中,可以在调用目标对象相应方法前后加上其他业务处理逻辑
- CGLIB是针对类实现代理,主要是对指定的类生成一个子类(没有实例化一个类),覆盖其中的方法
新生代由Eden区和Survivor区(S0、S1)组成,大小通过-Xmn参数指定,Eden与Survivor区的内存大小比例默认是8:1,可以通过-XX:SurvivorRatio参数指定
大多数情况下,对象在Eden中分配,当Eden没有足够空间时,会触发一次Minor GC,虚拟机提供了-XX:PintGCDetails参数,告诉虚拟机在发生垃圾回收时打印内存回收日志。
Survivor区是新生代和老年代之间的缓冲区域,当新生代发生Minor GC时,会将存活的对象移动到S0内存区域,并清空Eden区,当再次发生Minor GC时,将Eden和S0中存活的对象移动到S1内存区域。
存活对象会反复在S0和S1之间移动,当对象从Eden移动到Survivor或者在Survivior之间移动时,对象的GC年龄会自动累加,当GC年龄超过默认阈值15时,会将该对象移动到老年代,可以通过-XX:MaxTenuringThreshold对GC年龄的阈值进行设置。
老年代的空间大小即-Xmx和-Xmn两个参数之差,用于存放经过几次Minor GC之后依旧存活的对象。当老年代的空间不足时,将会触发Major GC,速度一般比Minor GC慢10倍以上。
类的元数据、方法信息(字节码、栈和变量大小)、运行时常量池、已确定的符号引用和虚方法表等被保存在永久代中,32位默认永久代的大小为64M,64位默认为85M,可以通过-XX:MaxPermSize进行设置,一旦类的元数据超过了永久代大小,就会跑出OOM异常。
当Full GC进行的时候,默认的方式是尽量清空新生代,因此在调用System.gc()时,新生代中存活的对象会提前进入老年代。Java 堆内存 新生代 (转)
高并发秒杀系统Java开发面试:高并发秒杀系统如何设计与优化。
SQL语句优化:
尽量避免在where字句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描
尽量避免在where字句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
1select id from t where num is null可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
1select id from t where num = 0使用exists代替in
用where代替having
drop、delete和truncate的区别
drop直接删除表,truncate删除表中数据,再插入时自增长id又从1开始,delete删除表中数据,可以加where字句。
对互斥锁加锁后,任何其他试图再对该互斥锁加锁的线程都会被阻塞直到当前持有锁的线程释放锁
当读写锁是写加锁时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞;当读写锁是读加锁时,所有试图以读模式对它进行加锁的线程都可以得到访问权
Struts2中Filter和Inteceptor的区别:
- filter是依赖于Servlet容器的,没有Servlet容器就没法回调doFilter方法,而Interceptor与Servlet无关
- Filter的过滤范围比Interceptor大,Filter除了过滤请求外通过通配符还可以保护页面、图片、文件等,而Interceptor只能过滤请求,只对action起作用
- Interceptor可以访问action上下文、值栈中的对象,而Filter不能
- 在action的生命周期中,拦截器可以被多次调用,而过滤器只能在容器初始化时被调用一次
假设有多台memcached服务器,编号分别为m0、m1、m2…,对于一个key,由客户端来决定存放到哪台机器,那最简单的hash公式就是key%N,其中N就是机器的总数。但这有个问题,一旦机器数变少,或者增加机器,N发生变化,那之前存放的数据就全部无效了,因为你按照新的N值取模计算出的计算编号,和当时按照旧的取模算出的机器编号肯定是不等的,也就意味着绝不部分缓存会失效。
这个问题的解决方法就是用1种特别的Hash函数,尽可能使得,增加机器/减少机器时,缓存失效的数目降到最低,这就是Hash环,或者叫一致性Hash。
上面说的Hash函数,只经历了一次hash,即把key hash到对应的机器编号,而hash环有两次hash:
- 把所有机器编号hash到这个环上
- 把key也hash到这个环上,然后在这个环上进行匹配,看这个key和哪台机器匹配
具体来说,假设有这样一个hash函数,其值空间为0~2^32-1,也就是说,其hash值是个32位无符号整型数字,这些数字形成一个环。
然后,先对机器进行hash(比如根据机器的IP),算出每台机器在这个环上的位置,再对key进行hash,算出该key在环上的位置,然后从这个位置走,遇到的第一台机器就是该key对应的机器,就把该(key, value)存储到该机器上。
首先计算出每台Cache服务器在环上的位置(图中的大圆圈);然后每来一个(key, value),计算出在环上的位置(图中的小圆圈),然后顺时针走,遇到的第1个机器,就是其要存储的机器。
这里的关键点是:当你增加/减少机器时,其他机器在环上的位置并不会发生改变。这样只有增加的那台机器、或者减少的那台机器附近的数据会失效,其他机器上的数据都还是有效的。
分布式锁
基于缓存实现分布式锁
使用缓存实现一方面性能较好,另一方面很多缓存是可以集群部署的,可以解决单点问题。
使用Zookeeper实现分布式锁
基于zookeeper临时有序节点实现分布式锁。每个客户端在对方法加锁时,在zookeeper上的与该方法对应的指定结点的目录下,生成一个唯一的瞬时有序结点,判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
- 锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
- 不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
- 单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
本小节参考分布式锁原理及实现方式。
分布式锁
- 当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
- 与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。(我觉得分布式情况下之所以问题变得复杂,主要就是需要考虑到网络的延时和不可靠。。。一个大坑)
- 分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。
redis的SETNX操作
其格式为:
SETNX key value
将key的值设为value,当且仅当key不存在;若给定的key已经存在,则SETNX不做任何动作。
使用SETNX实现分布式锁
多个进程执行以下Redis命令:
SETNX lock.foo <current Unix time + lock timeout + 1>
如果SETNX返回1,说明该进程获得锁,SETNX将键lock.foo的值设置为锁的超时时间(当前时间+锁的有效时间);
如果SETNX返回0,说明其他进程获得锁,进程不能进入临界区,进程可以在一个循环中不断的尝试SETNX操作,以获得锁。
两阶段提交(Two-phase Commit, 2PC)
两阶段提交协议经常被用来实现分布式事务,一般分为协调器和若干事务执行者两种角色,这里的事务执行者就是具体的数据库,协调者可以和事务执行器在一台机器上。
在分布式系统中,每个节点虽然可以知晓自己的操作是成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)。
我们设想从支付宝里转10000元到余额宝的场景:
首先我们的应用程序发起一个请求到协调器,然后由控制器来保证分布式事务
准备凭证阶段
协调器先将
<prepare>
消息写到本地日志向所有的参与者发起
<prepare>
消息,以支付宝转账到余额宝为例,协调器给A的prepare消息是通知支付宝数据库相应账目扣款10000,协调器给B的prepare消息是通知余额宝数据库相应账目增加10000。注:
为什么在执行任务前需要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证 的效果,如果没有本地日志(凭证),出问题容易死无对证参与者收到
<prepare>
消息后,执行具体本机事务,但不会进行commit,如果成功返回<yes>
,不成功返回<no>
。同理,返回前都应把要返回的消息写到日志里,当作凭证。支付宝:
123456789BEGIN WORKselect money from zhifubao where user='xiaorui' for update;update zhifubao set money=money-10000 where user='xiaorui';...Operation N向协调者发送YES或者NO !等待协调者的指令过来 !余额宝:
123456789BEGIN WORKselect money from yuebao where user='xiaorui' for update;update yuebao set money=money+10000 where user='xiaorui';...Operation N根据状态向协调者发送YES或者NO !然后等待协调者的指令过来 ... ....协调器收集所有执行器返回的消息,如果所有执行器都返回yes,那么给所有执行器发生送commit消息,执行器收到commit后执行本地事务的commit操作;如果有任一个执行器返回no,那么给所有执行器发送abort消息,执行器收到abort消息后执行事务abort操作。
123456#blog: xiaorui.ccif 协调者指令 == "to_commit":commit work #提交事务else:ROLLBACK #回滚
协调器或参与者把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。举个例子,比如某个参与者从故障中恢复后,先检查本机的日志,如果已收到
<commit >
,则提交,如果<abort >
则回滚。如果是<yes>
,则再向控制器询问一下,确定下一步。如果什么都没有,则很可能在<prepare>
阶段Si就崩溃了,因此需要回滚。
二阶段协议存在的问题是:
- 同步阻塞问题:执行过程中,所有参与节点都是事务阻塞型的,当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态
- 单点故障:由于协调者的重要性,一旦协调者发生故障,参与者将会一直阻塞下去,尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作
- 数据不一致:在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求,而这部分参与者接到commit请求之后就会执行commit操作,但是其他部分未收到commit请求的机器则无法执行事务提交,于是出现数据不一致的现象
本文转载自理解分布式事务的两阶段提交2pc。
RocketMQ事务消息实现分布式事务
和两阶段提交不同,RocketMQ事务消息实现分布式事务主要包含下面几个步骤:
- 发送Prepared消息
- update DB
- 根据update DB结果成功或失败,Confirm或者取消Prepared消息
如果前两步执行成功了,最后1步失败了怎么办?这里就涉及到RocketMQ的关键点:RocketMQ会定期(默认是1分钟)扫描所有的Prepared消息,询问发送方,到底是要确认这条消息发出去?还是取消此条消息?
具体实现如下:
也就是定义了一个checkListener,RocketMQ会回调此Listener,从而实现上面所述的方案。
面试总结。
enum定义的类默认继承的是java.lang.Enum类而不是Object类,同时注意枚举类不能派生子类(类的默认修饰符是final),其原因是它只有private构造器。
枚举类是包含有固定数量实例(并且实例的值也固定)的特殊类,如果其含有public构造器,那么在类的外部就可以通过这个构造器来新建实例,显然这时实例的数量和值不固定。
volatile修饰的变量进行写操作的时候会多一个lock前缀的指令,lock前缀的指令在多核处理器下会发生以下两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回操作会引起其他CPU里缓存了该内存地址的数据无效
select语句是不会对数据加锁,而select for update语句则会加行级锁:select for update和select for update wait和select for update nowait的区别。
System.gc()只是告诉JVM尽快GC一次,但不会立即执行GC,Java的GC是由JVM自动调用的,在需要的时候才执行。
并发是指两个或多个事件在同一时间间隔发生,并行是指两个或多个事件在同一时刻发生。